package riofrio;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.Set;

/**
 * A node in the three. It can have content and other nodes as children.
 * <p>
 * A child can not be added more than once.
 * </p>
 * <p>
 * Example to build a node:
 * </p>
 * <code>
 *    TreeNode.with("Name");
 * </code>
 * <p>
 * Example to set children in a node:
 * </p>
 * <code>
 * node.children(with("A"), with("B"));
 * </code>
 * @author Victor Hugo Herrera Maldonado
 */
public class TreeNode<T> implements Iterable<TreeNode<T>>{
    private T content;
    private final Set<TreeNode<T>> children=new LinkedHashSet<>();
    
    /**
     * Creates a node with the specified content.
     * 
     * @param content Content of the node.
     */
    protected TreeNode(T content) {
        this.content=content;
    }
    
    /**
     * Creates a node without content.
     */
    protected TreeNode() {
        
    }

    /**
     * Sets the content of this node.
     * 
     * @param content Content of the node. It can be null.
     * @return this node
     */
    public TreeNode<T> setContent(T content) {
        this.content = content;
        return this;
    }
    
    /**
     * Gets the content of this node.
     * 
     * @return The contents of this node. It can be null if the content was not set or null was passed as content. 
     */
    public T getContent() {
        return content;
    }
    
    /**
     * Checks if this node has children.
     * 
     * @return true only if this node has children.  
     */
    public boolean isParent() {
        return !children.isEmpty();
    }
    
    /**
     * Add a child to this node. If is already a child of this node, this call has no effect. 
     * 
     * @param child Node to add as child.
     * @return This node
     * @throws NullPointerException if child is null.
     */
    public TreeNode addChild(TreeNode<T> child) {
        Objects.requireNonNull(child, "child is null");
        children.add(child);
        return this;
    }
    
    /**
     * Sets the children of this node. If there were children previously, they are removed.
     * 
     * @param children The children nodes.
     * @return This node
     */
    public TreeNode<T> children(TreeNode<T>... children) {
        this.children.clear();
        this.children.addAll(Arrays.asList(children));
        return this;
    }
    
    /**
     * Creates children for the specified content. For each element in the contents, a node is created and added as a child
     * of this node.
     * 
     * @param contents Array of content.
     * @return This node.
     */
    public TreeNode<T> childrenWith(T... contents) {
        this.children.clear();
        for (T t : contents) {
            this.addChild(with(t));
        }
        return this;
    }
    
    /**
     * Remove the specified child if is present in this node.
     * 
     * @param child Child to remove.
     * @return this node.
     */
    public TreeNode<T> removeChild(TreeNode<T> child) {
        children.remove(child);
        return this;
    }
    
    /**
     * Remove the child at the specified index.
     * 
     * @param index Index of child to remove.
     * @return this node
     * @throws IndexOutOfBoundsException if index &lt; 0 or index &ge; getChildrenCount()
     */
    public TreeNode<T> removeChildAt(int index) {
        if(index < 0 || index >= children.size()) {
            throw new IndexOutOfBoundsException();
        }
        Iterator<TreeNode<T>> iterator = children.iterator();
        for(int i=0; iterator.hasNext(); i++) {
            iterator.next();
            if(index == i) {
                iterator.remove();
                break;
            }
        }
        return this;
    }
    
    /**
     * Removes all the children of this node.
     * 
     * @return this node
     */
    public TreeNode<T> removeAllChildren() {
        children.clear();
        return this;
    }
    
    /**
     * Gets the count of children for this node.
     * 
     * @return The count of children nodes.
     */
    public int getChildrenCount(){
        return children.size();
    }
    
    /**
     * Gets the child in the specified index.
     * 
     * @param index Index of child.
     * @return Child
     * @throws IndexOutOfBoundsException if index &lt; 0 or index &ge; getChildrenCount()
     */
    public TreeNode<T> getChildAt(int index) {
        if(index < 0 || index >= children.size()) {
            throw new IndexOutOfBoundsException();
        }
        Iterator<TreeNode<T>> iterator = children.iterator();
        for(int i=0; iterator.hasNext(); i++) {
            TreeNode<T> node = iterator.next();
            if(i == index) {
                return node;
            }
        }
        return null;
    }
    
    /**
     * Gets an iterator over the children of this node in the order that were added. 
     * Modifying the iterator doesn't affect the children of this node.
     * 
     * @return An iterator.
     */
    @Override
    public ListIterator<TreeNode<T>> iterator() {
        return new ArrayList<>(children).listIterator();
    }
    
    /**
     * Get an iterator over the children of this node in the reverse order that were added. 
     * Modifying the iterator doesn't affect the children of this node.
     * 
     * @return The iterator.
     */
    public ListIterator<TreeNode<T>> reverseIterator() {
        List<TreeNode<T>> list=new ArrayList<>(children);
        Collections.reverse(list);
        return list.listIterator();
    }
    
    /**
     * Creates a node with the specified content.
     * 
     * @param <C> The type of the content of the node
     * @param content Content of the node.
     * @return A node with the specified content.
     */
    public static <C> TreeNode<C> with(C content) {
        return new TreeNode<>(content);
    }
    
    /**
     * Creates a node without content. A parameter type is used because there can be children
     * nodes that does have content.
     * 
     * @param <C> The type used in the hierarchy.
     * @return A node without content.
     */
    public static <C> TreeNode<C> empty() {
        return new TreeNode<>();
    }
    
}
